package com.mspbots.web.config;

import com.mspbots.web.security.service.UsersService;
import lombok.RequiredArgsConstructor;
import lombok.extern.log4j.Log4j2;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.security.authentication.ReactiveAuthenticationManager;
import org.springframework.security.authentication.UserDetailsRepositoryReactiveAuthenticationManager;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.ReactiveUserDetailsService;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.security.web.server.WebFilterExchange;
import org.springframework.security.web.server.authentication.*;
import org.springframework.security.web.server.context.WebSessionServerSecurityContextRepository;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.List;

/**
 * com.mspbots.web.config.WebSecurityConfig
 *
 * @author Alex bob(https://github.com/vnobo)
 * @date Created by 2019/6/24
 */
@Log4j2
@Order(10)
@Configuration
@RequiredArgsConstructor
public class SecurityConfiguration {

    private final UsersService userRepository;
    private final PasswordEncoder passwordEncoder;

    @Bean
    public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
        http
                .csrf().disable()
                .addFilterAt(authenticationWebFilter(), SecurityWebFiltersOrder.HTTP_BASIC)
                .exceptionHandling()
                .authenticationEntryPoint(new CustomServerAuthenticationEntryPoint())
                .and()
                .authorizeExchange()
                .pathMatchers("/actuator/**", "/sso/**", "/register", "/tenant").permitAll()
                .pathMatchers("/api/tenant/configuration/**", "/api/tenant/exist-tenant/**", "/api/report/insertAndSendMail", "/api/tenant/microsoft-id").permitAll()
                .pathMatchers("/teamsweb/user/add-batch/{tenantId}", "/api/teams/user/**", "/datateams/oauth/redirect").permitAll()
                .pathMatchers("/wise/notice/**", "/wise/exists/*", "/wise/connect/*", "/wise/manual/*").permitAll()
                .pathMatchers("/attendance/tenantattendances/findExecl").permitAll()
                .pathMatchers("/datateams/**").permitAll()
                .anyExchange().authenticated();
        return http.build();
    }

    @Bean(autowireCandidate = false)
    public AuthenticationWebFilter authenticationWebFilter() {
        AuthenticationWebFilter authenticationFilter = new AuthenticationWebFilter(reactiveAuthenticationManager());
        authenticationFilter.setAuthenticationFailureHandler(new ServerAuthenticationEntryPointFailureHandler(new CustomServerAuthenticationEntryPoint()));
        authenticationFilter.setServerAuthenticationConverter(new ServerHttpBasicAuthenticationConverter());
        authenticationFilter.setSecurityContextRepository(new WebSessionServerSecurityContextRepository());
        authenticationFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler(this.userRepository));
        return authenticationFilter;
    }

    @Bean(autowireCandidate = false)
    public ReactiveAuthenticationManager reactiveAuthenticationManager() {
        return new CustomAuthenticationManager(userDetailsService(), this.passwordEncoder);
    }

    @Bean(autowireCandidate = false)
    public ReactiveUserDetailsService userDetailsService() {
        return (username) -> userRepository.loadUser(username.toLowerCase())
                .map(u -> User.withUsername(u.getUsername())
                        .password(u.getPassword())
                        .authorities(u.getAuthorities().toArray(new String[0]))
                        .accountExpired(!u.getEnabled())
                        .credentialsExpired(!u.getEnabled())
                        .disabled(!u.getEnabled())
                        .accountLocked(!u.getEnabled())
                        .build());
    }

    static class CustomAuthenticationManager extends UserDetailsRepositoryReactiveAuthenticationManager {

        CustomAuthenticationManager(ReactiveUserDetailsService userDetailsService,
                                    PasswordEncoder passwordEncoder) {
            super(userDetailsService);
            this.setPasswordEncoder(passwordEncoder);
        }

        @Override
        public Mono<Authentication> authenticate(Authentication authentication) {
            return super.authenticate(authentication);
        }
    }

    static class CustomServerAuthenticationEntryPoint extends HttpBasicServerAuthenticationEntryPoint {
        @Override
        public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException e) {
            List<String> requestedWith = exchange.getRequest().getHeaders().get("X-Requested-With");
            if (requestedWith != null && requestedWith.contains("XMLHttpRequest")) {
                return Mono.fromRunnable(() -> {
                    ServerHttpResponse response = exchange.getResponse();
                    response.setStatusCode(HttpStatus.UNAUTHORIZED);
                });
            }
            return super.commence(exchange, e);
        }
    }

    static class CustomAuthenticationSuccessHandler extends WebFilterChainServerAuthenticationSuccessHandler {

        private final UsersService userRepository;

        public CustomAuthenticationSuccessHandler(UsersService userRepository) {
            this.userRepository = userRepository;
        }

        @Override
        public Mono<Void> onAuthenticationSuccess(WebFilterExchange webFilterExchange,
                                                  Authentication authentication) {
            ServerWebExchange exchange = webFilterExchange.getExchange();
            return webFilterExchange.getChain().filter(exchange)
                    .doFirst(() -> this.userRepository.setLastLogin(authentication.getName()).subscribe());
        }
    }
}
